module loader.Cfg;


private {
//	import loader.Loader;
	import utils.fileWrapper.File;
	import utils.Memory;
	import maths.Vec;
	
	import std.conv;
	import std.string : toString, splitlines;
	
	import mintl.sortedaa : SortedAA;
	static import mintl.mem;
}


enum FieldType : byte {
	String,
	Number,
	Binary,
	Array,
	Boolean,
	None
}


template TypeToFieldType(T) {
			static if (is(T == char[]))	const FieldType TypeToFieldType = FieldType.String;
	else	static if (is(T == int))			const FieldType TypeToFieldType = FieldType.Number;
	else	static if (is(T == float))		const FieldType TypeToFieldType = FieldType.Number;
	else	static if (is(T == double))	const FieldType TypeToFieldType = FieldType.Number;
	else	static if (is(T == bool))		const FieldType TypeToFieldType = FieldType.Boolean;
	else	static assert (false);
}


struct Field {
	union {
		char[]	string;
		double	number;
		void[]	binary;
		Array*	array_;
		bool		boolean;
	}
	
	
	Array* array() {
		assert (FieldType.Array == type);
		return array_;
	}
	
	
	void array(Array a) {
		Array[] arr;
		if (array_ is null) arr.alloc(1);
		else arr = array_[0..1];
		arr[0] = a;
		array_ = &arr[0];
	}
	
	
	void array(Array* a) {
		array_ = a;
	}
	

	FieldType	type = FieldType.None;
	
	
	void destroy() {
		//printf("Field.destroy (%0.8x)\n", this);
		
		if (FieldType.String == type) {
			string.free();
		}
		else if (FieldType.Binary == type) {
			binary.free();
		}
		else if (FieldType.Array == type) {
			assert (array_ !is null);
			array_.destroy();
			Array[] a = array_[0..1];
			a.free();
			array_ = null;
		}
		
		type = FieldType.None;

		//printf("<- Field.destroy\n");
	}
	
	
	char[] toString() {
		switch (type) {
			case FieldType.String:	return "str:'" ~ string ~ "'";
			case FieldType.Number:	return "nbr:" ~ .toString(number);
			case FieldType.Binary:	return "binary:'" ~ .toString(binary.length);
			case FieldType.Array:		return "array:{\n" ~ array.toString ~ "}";
			case FieldType.Boolean:	return (boolean ? "bool:true" : "bool:false");
			case FieldType.None:		return "notype";
			default: assert (false);
		}
	}
	
	
	T get(T)() {
		static if (is(T == char[]))	return string;
		static if (is(T == int))			return cast(int)number;
		static if (is(T == float))		return cast(float)number;
		static if (is(T == double))	return number;
		static if (is(T == bool))		return boolean;
	}
}


struct FieldTuple {
	Field[]	fields;
	uint		numFields;
	
	
	void addField(Field f) {
		//fields ~= f;
		append(fields, f, &numFields);
	}
	
	
	void compress() {
		fields.realloc(numFields);
		
		foreach (inout f; fields) {
			if (FieldType.Array == f.type) {
				f.array.compress();
			}
		}
	}
	
	
	void destroy() {
		//printf("FieldTuple.destroy\n");

		foreach (inout f; fields[0 .. numFields]) {
			f.destroy();
		}
		
		fields.free();
		numFields = 0;

		//printf("<- FieldTuple.destroy\n");
	}
	
	
	char[] toString() {
		char[] result;
		foreach (i, inout f; fields[0 .. numFields]) {
			if (0 == i) result ~= f.toString();
			else result ~= " " ~ f.toString();
		}
		return result;
	}
}


struct Array {
	FieldTuple[]	fields;
	char[][]			fieldNames;
	uint				numFields;
	uint				numFieldNames;
	
	SortedAA!(char[], int[], false, mintl.mem.Malloc) namedFields;
	
	
	void addFieldTuple(inout FieldTuple ft, char[] name = null) {
		if (name !is null) {
			//namedFields[name] ~= fields.length;
			//append(namedFields[name], fields.length);
			int[]* nfields = namedFields.get(name);
			if (nfields is null) {
				int[] emptyArray;
				namedFields[name] = emptyArray;
				nfields = namedFields.get(name);
				assert (nfields !is null);
			}
			
			(*nfields).append(numFields);
		}
		
		//fields ~= ft;
		//fieldNames ~= name;
		append(fields, ft, &numFields);
		append(fieldNames, name, &numFieldNames);
	}
	
	
	void compress() {
		fields.realloc(numFields);
		fieldNames.realloc(numFieldNames);
		
		foreach (inout ft; fields) {
			ft.compress();
		}
	}
	
	
	void destroy() {
		debug printf("Array.destroy\n");

		foreach (inout n; fieldNames[0 .. numFieldNames]) {
			n.free();
		}
		
		fieldNames.free();
		numFieldNames = 0;
		
		
		foreach (inout int[] nf; namedFields) {
			nf.free();	
		}
		namedFields.clear();


		foreach (inout f; fields[0 .. numFields]) {
			f.destroy();
		}
		
		fields.free();
		numFields = 0;
		
		debug printf("<- Array.destroy\n");
	}
	
	
	char[] toString() {
		char[] result;
		foreach (i, f; fields[0 .. numFields]) {
			char[] data;
			
			if (fieldNames[i] !is null) {
				data ~= fieldNames[i] ~ ":";
			}
			
			data ~= f.toString();
			
			foreach (line; .splitlines(data)) {
				result ~= "  " ~ line ~ "\n";
			}
		}
		
		return result;
	}
	
	

	int count(char[] name) {
		if (namedFields.get(name) is null) return 0;
		return namedFields[name].length;
	}
	
	int count() {
		return numFields;
	}
	
	int tupleLength(char[] name, int index = 0) {
		return fields[namedFields[name][index]].numFields;
	}
	

	Array child(char[] name, int index = 0, int tupleIndex = 0) {
		auto item = fields[namedFields[name][index]].fields[tupleIndex];
		assert (FieldType.Array == item.type);
		return *item.array;
	}
	
	
	bool hasChild(char[] name, int index = 0, int tupleIndex = 0) {
		auto indices = namedFields.get(name);
		if (indices is null) return false;
		if (indices.length <= index) return false;
		auto tuple = fields[(*indices)[index]];
		if (tuple.numFields <= tupleIndex) return false;
		auto item = tuple.fields[tupleIndex];
		return FieldType.Array == item.type;
	}
	

	T simpleFieldAccess(T)(char[] name, int index = 0, int tupleIndex = 0) {
		assert (namedFields.get(name) !is null);
		assert (namedFields[name].length > index);
		assert (fields[namedFields[name][index]].numFields > tupleIndex);
		
		auto item = fields[namedFields[name][index]].fields[tupleIndex];
		assert (item.type == TypeToFieldType!(T), "'" ~ name ~ "' : " ~ .toString(cast(int)item.type) ~ " : ");
		return item.get!(T);
	}


	alias simpleFieldAccess!(char[])	string_;
	alias simpleFieldAccess!(int)		int_;
	alias simpleFieldAccess!(float)		float_;
	alias simpleFieldAccess!(double)	double_;
	alias simpleFieldAccess!(bool)		bool_;
	

	vec2 vec2_(char[] name, int index = 0, int tupleIndex = 0) {
		return vec2(float_(name, index, tupleIndex), float_(name, index, tupleIndex+1));
	}
	
	vec3 vec3_(char[] name, int index = 0, int tupleIndex = 0) {
		return vec3(float_(name, index, tupleIndex), float_(name, index, tupleIndex+1), float_(name, index, tupleIndex+2));
	}

	vec4 vec4_(char[] name, int index = 0, int tupleIndex = 0) {
		return vec4(float_(name, index, tupleIndex), float_(name, index, tupleIndex+1), float_(name, index, tupleIndex+2), float_(name, index, tupleIndex+3));
	}

	vec2i vec2i_(char[] name, int index = 0, int tupleIndex = 0) {
		return vec2i(int_(name, index, tupleIndex), int_(name, index, tupleIndex+1));
	}
	
	vec3i vec3i_(char[] name, int index = 0, int tupleIndex = 0) {
		return vec3i(int_(name, index, tupleIndex), int_(name, index, tupleIndex+1), int_(name, index, tupleIndex+2));
	}
	
	vec4i vec4i_(char[] name, int index = 0, int tupleIndex = 0) {
		return vec4i(int_(name, index, tupleIndex), int_(name, index, tupleIndex+1), int_(name, index, tupleIndex+2), int_(name, index, tupleIndex+3));
	}
	
	
	void simpleArrayAccess(T)(char[] name, T[] result, int tupleIndex = 0, int stride = 1) {
		int[] idxArray = namedFields[name];
		assert (idxArray.length * stride == result.length);
		int dst = 0;
		foreach (idx; idxArray) {
			assert (fields[idx].fields[tupleIndex].type == TypeToFieldType!(T));
			result[dst] = fields[idx].fields[tupleIndex].get!(T);
			dst += stride;
		}
	}
	
	
	alias simpleArrayAccess!(char[]) 	string_;
	alias simpleArrayAccess!(int)			int_;
	alias simpleArrayAccess!(float)		float_;
	alias simpleArrayAccess!(double)	double_;
	alias simpleArrayAccess!(bool)		bool_;


	void vectorArrayAccess(V)(char[] name, V[] result, int tupleIndex = 0, int stride = 1) {
		for (int i = 0; i < V.dim; ++i) {
			simpleArrayAccess!(V.flt)(name, (cast(V.flt*)result.ptr + i)[0..result.length * V.dim], tupleIndex + i, i + stride * V.dim);
		}
	}
	
	alias vectorArrayAccess!(vec2)	vec2_;
	alias vectorArrayAccess!(vec3)	vec3_;
	alias vectorArrayAccess!(vec4)	vec4_;
	alias vectorArrayAccess!(vec2i)	vec2i_;
	alias vectorArrayAccess!(vec3i)	vec3i_;
	alias vectorArrayAccess!(vec4i)	vec4i_;


	Array child(int index = 0, int tupleIndex = 0) {
		auto item = fields[index].fields[tupleIndex];
		assert (FieldType.Array == item.type);
		return *item.array;
	}
	
	T simpleIndexedFieldAccess(T)(int index = 0, int tupleIndex = 0) {
		auto item = fields[index].fields[tupleIndex];
		assert (TypeToFieldType!(T) == item.type);
		return item.get!(T);
	}
	
	
	alias simpleIndexedFieldAccess!(char[])	string_;
	alias simpleIndexedFieldAccess!(int)		int_;
	alias simpleIndexedFieldAccess!(float)		float_;
	alias simpleIndexedFieldAccess!(double)	double_;
	alias simpleIndexedFieldAccess!(bool)		bool_;

	
	vec2 vec2_(int index = 0, int tupleIndex = 0) {
		return vec2(float_(index, tupleIndex), float_(index, tupleIndex+1));
	}
	
	vec3 vec3_(int index = 0, int tupleIndex = 0) {
		return vec3(float_(index, tupleIndex), float_(index, tupleIndex+1), float_(index, tupleIndex+2));
	}

	vec4 vec4_(int index = 0, int tupleIndex = 0) {
		return vec4(float_(index, tupleIndex), float_(index, tupleIndex+1), float_(index, tupleIndex+2), float_(index, tupleIndex+3));
	}

	vec2i vec2i_(int index = 0, int tupleIndex = 0) {
		return vec2i(int_(index, tupleIndex), int_(index, tupleIndex+1));
	}
	
	vec3i vec3i_(int index = 0, int tupleIndex = 0) {
		return vec3i(int_(index, tupleIndex), int_(index, tupleIndex+1), int_(index, tupleIndex+2));
	}
	
	vec4i vec4i_(int index = 0, int tupleIndex = 0) {
		return vec4i(int_(index, tupleIndex), int_(index, tupleIndex+1), int_(index, tupleIndex+2), int_(index, tupleIndex+3));
	}



	void simpleIndexedArrayAccess(T)(T[] result, int tupleIndex = 0, int stride = 1) {
		assert (numFields * stride == result.length, .toString(numFields * stride) ~ ` == ` ~ .toString(result.length));
		int dst = 0;
		foreach (idx, field; fields[0 .. numFields]) {
			assert (field.fields[tupleIndex].type == TypeToFieldType!(T));
			result[dst] = field.fields[tupleIndex].get!(T);
			dst += stride;
		}
	}
	
	alias simpleIndexedArrayAccess!(char[])		string_;
	alias simpleIndexedArrayAccess!(int)			int_;
	alias simpleIndexedArrayAccess!(float)		float_;
	alias simpleIndexedArrayAccess!(double)		double_;
	alias simpleIndexedArrayAccess!(bool)		bool_;


	void vectorIndexedArrayAccess(V)(V[] result, int tupleIndex = 0, int stride = 1) {
		for (int i = 0; i < V.dim; ++i) {
			simpleIndexedArrayAccess!(V.flt)((cast(V.flt*)result.ptr + i)[0..result.length * V.dim], tupleIndex + i, stride * V.dim);
		}
	}
	
	alias vectorIndexedArrayAccess!(vec2)	vec2_;
	alias vectorIndexedArrayAccess!(vec3)	vec3_;
	alias vectorIndexedArrayAccess!(vec4)	vec4_;
	alias vectorIndexedArrayAccess!(vec2i)	vec2i_;
	alias vectorIndexedArrayAccess!(vec3i)	vec3i_;
	alias vectorIndexedArrayAccess!(vec4i)	vec4i_;
}



class CfgLoader {
	CfgLoader load(char[] filename) {
		File srcFile = new File(filename);
		scope (exit) {
			srcFile.close();
			delete srcFile;
		}
		
		getc = delegate char() {
			if (srcFile.eof) {
				_end = true;
				return char.init;
			}
			
			char x = '\r';
			while ('\r' == x) {
				x = srcFile.getc();
			}
			return x;
		};
		
		parseArray(root, char.init, true);
		root.compress();
		
		return this;
	}
	
	
	void loadFromMem(char[] data) {
		getc = delegate char() {
			if (data.length > 0) {
				char res = data[0];
				data = data[1..$];
				//writefln(res);
				return res;
			} else {
				_end = true;
				return char.init;
			}
		};
		
		parseArray(root, char.init, true);
		root.compress();
	}
	
	
	alias void function(inout Array param, inout FieldTuple fieldTuple) DirectiveHandler;
	
	
	void addCustomDirective(char[] dname, DirectiveHandler handler) {
		customDirectives[dname] = handler;
	}


	Array result() {
		return root;
	}


	~this() {
		root.destroy();
		destroyWindowBuffer();
	}


protected:


	char delegate()				getc;
	DirectiveHandler[char[]]	customDirectives;

	
	void parseArray(inout Array array, char blockEnd, bool topLevel = false) {
		while (true) {
			//writefln("white2<");
			skipWhite();
			//writefln(">white2");
			
			if (inputEnd) {
				if (!topLevel) {
					inputEndError();
				} else {
					return;
				}
			}
			
			char firstChar = window(0);
			
			if ('\n' == firstChar || ',' == firstChar) {
				discardWindow(1);
			}
			else if (blockEnd == firstChar) {
				discardWindow(1);
				return;
			}
			else if (validNameChar(firstChar, true) && !curIdent("true") && !curIdent("false")) {
				// a named value
				char[] name = parseName();
				skipWhite();
				
				//printf("name: %.*s\n", name);
				
				if (window(0) == '=') {
					discardWindow(1);
					skipWhite();
					
					FieldTuple f;
					parseValue(f);
					array.addFieldTuple(f, name);
				} else {		// no value, just a name
					FieldTuple f;
					array.addFieldTuple(f, name);
				}
			}
			else {
				//writefln("noname field");
				FieldTuple f;
				parseValue(f);
				array.addFieldTuple(f, null);
			}
		}
		
		assert (topLevel);
	}
	
	
	bool curIdent(char[] str) {
		foreach (i, c; str) {
			if (window(i) != c || inputEnd) return false;
		}
		return !validNameChar(window(str.length), false);
	}
	
		
	void parseValue(inout FieldTuple fieldTuple) {
		while (!inputEnd) {
			char firstChar = window(0);
	
			if ('-' == firstChar || '.' == firstChar || (firstChar >= '0' && firstChar <= '9')) {
				//writefln("parsing a nbr");
				
				int lim = 1;
				while (!inputEnd && validNumberChar(window(lim))) {
					++lim;
				}
				
				Field field;
				field.number = toDouble(window(0, lim));
				field.type = FieldType.Number;
				fieldTuple.addField(field);
				
				//writefln("got a nbr: ", field.number);
				
				discardWindow(-1);
			} else
			
			if ('(' == firstChar) {
				discardWindow(1);
				char[] directiveName = parseName();
				Array param;
				parseArray(param, ')');
				
				processDirective(directiveName, param, fieldTuple);
				directiveName.free();
				param.destroy();
			} else
			
			if ('{' == firstChar) {
				discardWindow(1);
				
				Array param;
				parseArray(param, '}');
				
				processDirective(`array`, param, fieldTuple);
				param.destroy();
			} else
			
			if ('\'' == firstChar) {
				// string
				Field field;
				field.type = FieldType.String;
				
				uint stringLen;
				
				discardWindow(1);
				
				void addChar(char c) {
					/+field.string.realloc(field.string.length + 1);
					field.string[$-1] = c;+/
					field.string.append(c, &stringLen);
				}
				
				bool escaped = false;
				while (true) {
					if ('\\' == window(0)) {
						discardWindow(1);

						if (escaped) {
							addChar('\\');
						}
						
						escaped = !escaped;
					}
					
					else if ('\'' == window(0)) {
						discardWindow(1);
						
						if (escaped) {
							escaped = false;
							addChar('\'');
						} else {
							break;
						}
					}
					
					else {
						if (escaped) {
							addChar('\\');
							escaped = false;
						}
						addChar(window(0));
						discardWindow(1);
					}
				}
				
				field.string.realloc(stringLen);
				fieldTuple.addField(field);
				//printf("got a str: '%.*s'\n", field.string);
	
			} else
			
			if (curIdent("true")) {
				Field field;
				field.boolean = true;
				field.type = FieldType.Boolean;
				fieldTuple.addField(field);
				discardWindow(4);
			} else
			
			if (curIdent("false")) {
				Field field;
				field.boolean = false;
				field.type = FieldType.Boolean;
				fieldTuple.addField(field);
				discardWindow(5);
			} else

			if ('}' == firstChar) {
				// end of data directive
				return;
			} else
			
			if (')' == firstChar) {
				// end of directive != data
				return;
			} else
			
			if (',' == firstChar || '\n' == firstChar) {
				// next array item
				return;
			} else
			
			//writefln("white<");
			skipWhite();
			//writefln(">white");
		}
	}
	
	
	void processDirective(char[] directiveName, inout Array param, inout FieldTuple fieldTuple) {
		switch (directiveName) {
			case `array`: {
				Field f;
				f.type	= FieldType.Array;
				f.array	= param;
				fieldTuple.addField(f);
				param = Array.init;		// do not let destroy() be called on it
			} break;
			
			case `include`: {
				CfgLoader ldr = new CfgLoader;
				
				assert (param.numFields == 1);
				assert (param.fields[0].numFields == 1);
				
				assert (FieldType.String == param.fields[0].fields[0].type);
				
				//writefln("include<");
				ldr.load(param.fields[0].fields[0].string);
				//writefln(">include");
				
				Field f;
				f.type	= FieldType.Array;
				f.array	= ldr.result;
				fieldTuple.addField(f);
				
				ldr.root = Array.init;
			} break;
			
			case `flatten`: {
				foreach (inout tuple; param.fields[0 .. param.numFields]) {
					foreach (inout field; tuple.fields[0 .. tuple.numFields]) {
						flattenAndErase(field, fieldTuple);
						field = Field.init;
					}
				}
			} break;
			
			default: {
				if (directiveName in customDirectives) {
					return customDirectives[directiveName](param, fieldTuple);
				}
				
				assert (false);
			}
		}
	}
	
	
	static void flattenAndErase(Field f, inout FieldTuple fieldTuple) {
		if (FieldType.Array != f.type) {
			fieldTuple.addField(f);
		} else {
			foreach (inout tuple; f.array.fields[0 .. f.array.numFields]) {
				foreach (inout field; tuple.fields[0 .. tuple.numFields]) {
					flattenAndErase(field, fieldTuple);
					field = Field.init;
				}
			}
		}
	}
	
	
	void skipWhite() {
		void skipLine() {
			while (!inputEnd) {
				if (window(0) == '\n') {
					return;
				}
				
				discardWindow(1);
			}
		}
		
		void skipBlock() {
			int depth = 1;
			
			while (!inputEnd) {
				if (window(0) == '+') {
					if (window(1) == '/') {
						discardWindow(2);
						--depth;
						if (0 == depth) return;
						else continue;
					}
				}
				
				else if (window(0) == '/') {
					if (window(1) == '+') {
						discardWindow(2);
						++depth;
						continue;
					}
				}

				discardWindow(1);
			}
		}
		
		while (!inputEnd) {
			if (window(0) == ' ' || window(0) == '\t') {
				discardWindow(1);
				continue;
			}
			
			if (window(0) == '/') {
				if (window(1) == '/') {
					return skipLine();
				}
				
				if (window(1) == '+') {
					discardWindow(2);
					skipBlock();
					continue;
				}
			}
			
			break;
		} 
	}
	
	
	bool inputEnd() {
		return _end;
	}
	
	
	char[] parseName() {
		char[] res;
		uint nameLen = 0;
		
		for (int i = 0; true; ++i) {
			char c = window(i);
			if (inputEnd) break;
			
			if (validNameChar(c, 0 == i)) {
				append(res, c, &nameLen);
			} else {
				break;
			}
		}
		
		assert (nameLen > 0);
		assert (res !is null);
		debug printf("realloc from %d to %d, ptr=%0.8x\n", res.length, nameLen, res.ptr);
		res.realloc(nameLen);
		
		discardWindow(-1);
		return res;
	}
	
	
	bool validNameChar(char c, bool first = false) {
		return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (!first && c >= '0' && c <= '9')|| c == '_';
	}


	bool validNumberChar(char c, bool first = false) {
		return (c >= '0' && c <= '9') || c == '.' || c == '-' || (!first && (c == '_' || c == 'e' || c == '+'));
	}
	
	
	void inputEndError() {
		assert (false);
	}
	
	
	/**
		Params:
			offset = offset for window.length
	*/
	void discardWindow(int offset) {
		assert (_windowBuffer.ptr + _windowStart == _window.ptr);
		
		if (offset > 0) {
			_windowStart += offset;
			_window = _window[offset .. $];
		} else {
			_windowStart += _window.length + offset;
			_window = _window[$+offset .. $];
		}
		
		if (0 == _window.length) {
			_window = _windowBuffer[0..0];
			_windowStart = 0;
		} else {
			assert (_windowBuffer.ptr + _windowStart == _window.ptr);
		}
	}
	
	
	void destroyWindowBuffer() {
		_windowBuffer.free();
		_window = null;
	}
	
	
	char window(int idx) {
		// make sure we have this char
		// track the lines and character indices of beginning and end of window
		
		assert (idx >= 0);
		
		if (_windowBuffer.length <= idx + _windowStart) {
			_windowBuffer.realloc(idx + _windowStart + 1);
		}
		
		if (_window.length <= idx) {
			int fillFrom = _window.length;
			_window = _windowBuffer[_windowStart .. _windowStart + idx + 1];
			
			foreach (inout c; _window[fillFrom .. $]) {
				c = getc();
			}
		}
		
		return _window[idx];
	}
	
	
	char[] window(int start, int postEnd) {
		return _window[start .. postEnd];
	}


	Array	root;
	char[]	_window;
	int		_windowStart;
	bool		_end = false;
	
	char[]	_windowBuffer;
}
